package parser;

import java.util.*;

import common.AltaRicaModelManager;

import math.Expression;
import math.Relation;
import model.AltaRicaDomain;
import model.AltaRicaNode;
import model.altarica_fields.Sync.SyncElement;
import model.altarica_fields.*;
import util.BinaryTree;

public class AltaRicaParseNode {

    private String name;
    private List<Var> state_list;
    private List<Var> flow_list;
    private List<Event> event_list;
    private List<String[]> sub_list;
    private List<Sync> sync_list;
    private List<Trans> trans_list;
    private List<UpdateExpression> init_list;
    private List<Expression> assert_list;
    private List<String[]> priorites_list;
    private AltaRicaNode anode;
    private boolean compiled = false;
    private int _line = 0;
    

    public class CycleDetectionException extends Exception {

		public CycleDetectionException(String nodeA, String nodeB) {
			this._nodeA = nodeA;
			this._nodeB = nodeB;
		}
		private String _nodeA = "";
		private String _nodeB = "";
		public String getMessage() {
			return "Cycle detection between " + _nodeA + " and " + _nodeB;
		}
		private static final long serialVersionUID = 1L;
    	
    }
    public AltaRicaParseNode() {
 
        name = null;
        
        state_list = new LinkedList<Var>();
        flow_list = new LinkedList<Var>();
        event_list = new LinkedList<Event>();
        sub_list = new LinkedList<String[]>();
        sync_list = new LinkedList<Sync>();
        trans_list = new LinkedList<Trans>();
        init_list = new LinkedList<UpdateExpression>();
        assert_list = new LinkedList<Expression>();
        priorites_list = new LinkedList<String[]>();

        anode = null;
    }
    
    public List<Trans> getTrans()
    {
        return Collections.unmodifiableList(this.trans_list);
    }
    
    public List<Var> getFlows()
    {
        return Collections.unmodifiableList(this.flow_list);
    }

    public List<Var> getStates()
    {
        return Collections.unmodifiableList(this.state_list);
    }
    
    public List<Sync> getSyncs()
    {
        return Collections.unmodifiableList(this.sync_list);
    }

    public List<Event> getEvents()
    {
        return Collections.unmodifiableList(this.event_list);
    }

    public void setName(String name) {

        this.name = name;
    }

    public String getName() {

        return name;
    }

    public void addState(Var v) {

        state_list.add(v);
    }

    public void addFlow(Var v) {

        flow_list.add(v);
    }

    public void addEvent(String id) {

        event_list.add(new Event(id));
    }

    public void addSub(String sub[]) {

        sub_list.add(sub);
    }

    public void addSync(Sync sync) {

        sync_list.add(sync);
    }

    public void addTrans(Trans t) {

        trans_list.add(t);
    }

    public void addInit(UpdateExpression u) {

        init_list.add(u);
    }

    public void addPriorities(List<String> less, List<String> more) {

        String str[] = new String[2];

        for (String l : less) {
            for (String m : more) {
                str = new String[2];
                str[0] = l;
                str[1] = m;
                priorites_list.add(str);
            }
        }
    }

    public void addAssert(Expression expr) {

        assert_list.add(expr);
    }

    public boolean isCompiled() {

        return this.compiled;
    }

    public AltaRicaNode getNode() {

        return this.anode;
    }


    
    public AltaRicaNode compile(AltaRicaParseModel pmodel) throws CycleDetectionException {

    	
        AltaRicaNode node = new AltaRicaNode();

        this.anode = node;

        HashMap<String, Var> states = new HashMap<String, Var>();
        HashMap<String, Var> flow = new HashMap<String, Var>();
        HashMap<String, Event> events = new HashMap<String, Event>();

        node.setName(this.name);

        for (Var v : state_list) {

            compileVar(pmodel, node, v);
            if (states.put(v.getId(), v) != null) {
                System.out.println("Duplicate Variable " + v);
            }
        }

        node.setStates(states);

        for (Var v : flow_list) {

            compileVar(pmodel, node, v);
            if ((flow.put(v.getId(), v) != null) || states.containsKey(v.getId())) {
                System.out.println("Duplicate Variable " + v);
            }
        }

        node.setFlows(flow);

        for (Event e : event_list) {
        	if (e.getId() != null && !e.getId().equals("")) {
        		events.put(e.getId(), e);
        	}
        }

        if (events.containsKey(Event.getEpsilon().getId()) == false) {
        	if (Event.getEpsilon().getId() != null && !Event.getEpsilon().getId().equals("")) {
        		events.put(Event.getEpsilon().getId(), Event.getEpsilon());
        	}
        }
        node.setEvents(events);
        
        

        compilePriorities(pmodel, node);
        
        for (String[] str : sub_list) {

            AltaRicaNode sub = null;

            AltaRicaParseNode pnode = pmodel.getParsedNodes().get(str[1]);
            boolean found = false;
            for (String s : pmodel.getSubNodesCompiled())
            {
            	if (s.equals(str[1] + " - " + this.getName())) found = true;
            }
            if (!found) {
            	pmodel.getSubNodesCompiled().add(str[1] + " - " + this.getName());
            	
            } 
            else {
            	throw new CycleDetectionException(pnode.getName(), this.getName());
            }
            if (pnode != null && pnode.isCompiled() == false) {
                sub = pnode.compile(pmodel);
                
            } else {
                if (pmodel.getParsedNodes().get(str[1]) != null)
                {
                	sub = pmodel.getParsedNodes().get(str[1]).anode;
                }
            }
            node.addSub(str[0], sub);
          
        }

        for (Sync sync : sync_list) {

            compileSync(pmodel, node, sync);
        }

        compileUpdates(node, init_list, true);

        node.setInit(init_list);

        compileTransitions(node);

        node.setTrans(trans_list);

        for (Expression expr : assert_list) {

            compileExpression(node, expr);
        }

        node.setAsserts(assert_list);

        
        this.compiled = true;
        node.setLine(getLine());
        return node;
    }

    
    public void compileRelation(Relation p) {

        AltaRicaNode node = AltaRicaModelManager.getCurrentModel().getNode(p.getNode().getName());

        if (node == null) {
            System.out.println("Error: unknown AltaRica node: " + p.getNode().getName());
            return;
        }

        p.setNode(node);

        BinaryTree tree = p.getExpression().getExpression();
        p.setModel(AltaRicaModelManager.getCurrentModel());
        compileRelationExpression(node, tree);
    }

    private void compilePriorities(AltaRicaParseModel pmodel, AltaRicaNode node) {

        for (String str[] : priorites_list) {

            Event a = node.getEvent(str[0]);
            Event b = node.getEvent(str[1]);

            b.addLessEvent(a);
        }
    }

    private void compileSync(AltaRicaParseModel pmodel, AltaRicaNode node, Sync sync) {

        AltaRicaNode sub = node;


        ArrayList<String> ids = null;

        String evid = null;

        for (SyncElement synce : sync.getSyncs()) {

            sub = node;

            ids = synce.getSubs();

            evid = ids.get(ids.size() - 1);
            ids.remove(ids.size() - 1);

            for (String id : ids) {
                sub = sub.getSub(id);
                if (sub == null) {
                    System.out.println("Missing Sub " + id);
                    return;
                }
            }

            Event e = sub.getEvent(evid);

            if (e == null) {
                System.out.println("Unknown Event " + evid + " in " + sync);
            }

            sync.setEvent(synce, e);
        }

        node.addSync(sync);
    }

    private void compileVar(AltaRicaParseModel pmodel, AltaRicaNode node, Var var) {

        AltaRicaDomain domain;
        VarType type = var.getType();

        var.setNode(node);

        if (type.getVarType() == VarType.DOMAIN) {

            String id = type.getDomain();

            domain = pmodel.getDomain(id);

            if (domain == null) {

                System.out.println("Unknown domain: " + id);
            }
            
            var.setType(domain.getType());
        }
    }

    private void compileTransitions(AltaRicaNode node) {


        Expression expr;

        for (Trans t : trans_list) {

            List<Event> clst = new LinkedList<Event>();

            expr = t.getGuard();

            compileExpression(node, expr);

            compileUpdates(node, t.getUpdates(), false);


            for (Event e : t.getEvent()) {

                clst.add(node.getEvent(e));
            }

            if (clst.isEmpty()) {
                System.out.println("Missing Event");
            }

            t.setEvents(clst);
        }
    }

    private void compileUpdates(AltaRicaNode node, Collection<UpdateExpression> lst, boolean isInit) {

        Var updated = null;

        for (UpdateExpression u : lst) {

            updated = u.getVar();

            if (updated.isLocal() == false) {
                if (isInit == false) {
                    System.out.println("Error updating remote var " + updated.getId());
                }
                compileRemoteVar(node, updated, isInit, true);
            } else {
                updated = node.getStateVar(updated.getId());
            }

            if (updated == null) {
                System.out.println("Error: non state variable update: " + u.getVar().getId());
            } else {
                u.setVar(updated);
                compileExpression(node, u.getExpression());
            }
        }
    }

    private void compileExpression(AltaRicaNode node, Expression expr) {

        BinaryTree tree = expr.getExpression();

        compileExpressionTree(node, tree);
    }

    private void compileExpressionTree(AltaRicaNode node, BinaryTree tree) {

        Var var = null;

        LinkedList<BinaryTree> list = new LinkedList<BinaryTree>();

        list.add(tree);

        while (list.isEmpty() != true) {

            tree = list.poll();

            Object value = tree.getValue();

            if (value instanceof Var) {

                var = (Var) value;

                if (var.isLocal() == false) {
                    compileRemoteVar(node, var, true, true);
                } else {
                    var = node.getVar(((Var) value).getId());
                }

                if (var == null) {
                    tree.setValue(((Var) value).getId());
                } else {
                    tree.setValue(var);
                }
            }

            if (tree.getLeft() != null) {
                list.addLast(tree.getLeft());
            }

            if (tree.getRight() != null) {
                list.addLast(tree.getRight());
            }
        }
    }

    private void compileRemoteVar(AltaRicaNode node, Var var, boolean checkVisibility, boolean link) {

        Var remoteVar = null;
        AltaRicaNode sub = node;
        List<String> lst = var.getSubs();

        for (String str : lst) {
            sub = sub.getSub(str);
            if (sub == null) {
                System.out.println("Missing Sub " + str);
                return;
            }
        }

        remoteVar = sub.getVar(var.getName());

        if (remoteVar == null) {
            System.out.println("Missing Sub Var:" + var.getId() + " in: " + sub.getName());
            return;
        }

        if (checkVisibility) {
            if (remoteVar.isPrivate()) {
                System.out.println("Error: manipulating private var: " + remoteVar);
            } else if (remoteVar.isParent() & lst.size() != 1) {
                System.out.println("Error: manipulating parent var: " + remoteVar + " in non parent node.");
            }
        }
        var.setRemoteVar(remoteVar);

        if (link == true) {
            remoteVar.addRemoteReference(node);
        }
    }

    private void compileRelationExpression(AltaRicaNode node, BinaryTree tree) {

        Var var = null;

        LinkedList<BinaryTree> list = new LinkedList<BinaryTree>();

        list.add(tree);

        while (list.isEmpty() != true) {

            tree = list.poll();

            Object value = tree.getValue();

            if (value instanceof Var) {

                var = (Var) value;

                if (var.isLocal() == false && var.getSubs().size() > 1) {
                    String str = var.getSubs().remove(0);
                    compileRemoteVar(node, var, false, false);
                    var.getSubs().add(0, str);
                } else {
                    var = node.getVar(((Var) value).getId());
                }

                if (var == null) {
                    tree.setValue(((Var) value).getId());
                } else {
                    tree.setValue(var);
                }
            }

            if (tree.getLeft() != null) {
                list.addLast(tree.getLeft());
            }

            if (tree.getRight() != null) {
                list.addLast(tree.getRight());
            }
        }
    }

    @Override
    public String toString() {

        String str = "node " + name + "\n";

        str += "   sub\n";
        for (String s[] : sub_list) {
            str += "     " + s[0] + " : " + s[1] + ";\n";
        }

        str += "   state\n";
        for (Var s : state_list) {
            str += "     " + s + ";\n";
        }

        str += "   flow\n";
        for (Var s : flow_list) {
            str += "     " + s + ";\n";
        }

        str += "   event\n" + "     ";
        for (Event e : event_list) {
            str += e;
        }
        str += ";\n";

        str += "   trans\n";
        for (Trans t : trans_list) {
            str += "     " + t.toString() + ";\n";
        }

        str += "   init\n";
        for (UpdateExpression u : init_list) {
            str += "     " + u + ";\n";
        }

        str += "   assert\n";
        for (Expression e : assert_list) {
            str += "     " + e + ";\n";
        }

        str += "edon\n";

        return str;
    }

	public void setLine(int line) {
		this._line = line;
	}

	public int getLine() {
		return _line;
	}
}
